package com.semp.gu.connector.ssh;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringWriter;
import java.net.ConnectException;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.UserInfo;
import com.semp.gu.config.IConfigurationManager;
import com.semp.gu.connector.IConnector;
import com.semp.gu.domain.local.LocalAlbum;
import com.semp.gu.domain.local.LocalAlbumElement;
import com.semp.gu.domain.local.LocalAlbumElement.AlbumElementType;
import com.semp.gu.domain.remote.RemoteAlbum;
import com.semp.gu.domain.remote.RemoteGallery;

public class SshConnector implements IConnector {

	private SshConfiguration		configuration;
	private SshConfiguration		defaultConfiguration;
	private IConfigurationManager	configurationManager;
	private Session					session;

	// Logger
	private final static Logger		logger	= LoggerFactory.getLogger(SshConnector.class);

	// ----------------- Interface Methods ------------------

	public RemoteGallery getGallery() throws IOException {
		// Connect
		connectOrUseConnection();
		// Get folder tree
		String command = SshCommands.TREE.format(new String[] { configuration.getRootAlbumPath() });
		logger.debug("Executing remote command {}", command);
		String galleryTreeAsString = execCommand(command);
		// Parse folder tree
		RemoteGallery gallery = DomainFactory.parseGalleryTree(galleryTreeAsString);

		return gallery;
	}

	public void createAlbum(RemoteAlbum newAlbum, RemoteAlbum mainAlbum) throws IOException {
		// Connect
		connectOrUseConnection();
		// Create new folder
		mainAlbum.getSubAlbums().add(newAlbum);
		newAlbum.setParent(mainAlbum);
		String folderPath = configuration.getRootAlbumPath() + DomainFactory.computeAlbumPath(newAlbum);
		String command = SshCommands.CREATE_FOLDER.format(new String[] { folderPath });
		logger.debug("Executing remote command {}", command);
		execCommand(command);
	}

	public void uploadFiles(LocalAlbum localAlbum, RemoteAlbum remoteAlbum) throws IOException {
		// Connect
		connectOrUseConnection();
		// UploadFile
		String folderPath = configuration.getRootAlbumPath() + DomainFactory.computeAlbumPath(remoteAlbum);
		List<LocalAlbumElement> localElementList = localAlbum.getElements();
		for (LocalAlbumElement localAlbumElement : localElementList) {
			if (localAlbumElement.getType() != AlbumElementType.UNKNOWN) {
				List<File> convertedFileList = localAlbumElement.getConvertedFileList();
				for (File convertedFile : convertedFileList) {
					uploadFile(convertedFile, folderPath + DomainFactory.FILE_SEP + convertedFile.getName());
				}
			}
		}
	}
	
	public void uploadFile(LocalAlbumElement localAlbumElement, RemoteAlbum remoteAlbum) throws IOException {
		// Connect
		connectOrUseConnection();
		// UploadFile
		String folderPath = configuration.getRootAlbumPath() + DomainFactory.computeAlbumPath(remoteAlbum);
		if (localAlbumElement.getType() != AlbumElementType.UNKNOWN) {
			List<File> convertedFileList = localAlbumElement.getConvertedFileList();
			for (File convertedFile : convertedFileList) {
				uploadFile(convertedFile, folderPath + DomainFactory.FILE_SEP + convertedFile.getName());
			}
		}
	}

	// ----------------- Usefull Methods ------------------

	private void uploadFile(File localFile, String remoteFilePath) throws ConnectException {
		try {
			// exec 'scp -t rfile' remotely
			String command = SshCommands.COPY.format(new String[] { remoteFilePath });
			logger.debug("Executing remote command {}, using file {}", command, localFile);
			Channel channel = session.openChannel("exec");
			((ChannelExec) channel).setCommand(command);

			// get I/O streams for remote scp
			OutputStream out = channel.getOutputStream();
			InputStream in = channel.getInputStream();

			channel.connect();

			if (checkAck(in) != 0) {
				throw new ConnectException();
			}

			// send "C0644 filesize filename",
			command = "C0644 " + localFile.length() + " " + localFile.getName() + "\n";
			out.write(command.getBytes());
			out.flush();
			if (checkAck(in) != 0) {
				throw new ConnectException();
			}

			// send a content of lfile
			FileInputStream fis = new FileInputStream(localFile);
			byte[] buf = new byte[1024];
			while (true) {
				int len = fis.read(buf, 0, buf.length);
				if (len <= 0)
					break;
				out.write(buf, 0, len); // out.flush();
			}
			fis.close();
			fis = null;
			// send '\0'
			buf[0] = 0;
			out.write(buf, 0, 1);
			out.flush();
			if (checkAck(in) != 0) {
				throw new ConnectException();
			}
			out.close();
			channel.disconnect();

		} catch (Exception e) {
			logger.error("", e);
			// try{if(fis!=null)fis.close();}catch(Exception ee){}
			throw new ConnectException(e.getMessage());
		}
	}

	private static int checkAck(InputStream in) throws IOException {
		int b = in.read();
		// b may be 0 for success, 1 for error, 2 for fatal error, -1
		if (b != 0) {
			StringBuffer sb = new StringBuffer();
			int c;
			do {
				c = in.read();
				sb.append((char) c);
				if (sb.toString().length() > 1000) {
					logger.error(sb.toString());
					sb = new StringBuffer();
				}
			} while (c != '\n');
			logger.error("Error in upload process", sb.toString());
		}
		return b;
	}

	private String execCommand(String command) throws ConnectException {
		try {
			Channel channel = session.openChannel("exec");
			((ChannelExec) channel).setCommand(command);
			InputStream in = channel.getInputStream();
			channel.connect();

			StringWriter writer = new StringWriter();

			// Lecture par segment de 0.5Mo
			byte buffer[] = new byte[2048];
			int byteRead;

			while ((byteRead = in.read(buffer)) != -1) {
				writer.write(new String(buffer), 0, byteRead);
			}

			channel.disconnect();

			return writer.toString();

		} catch (Exception e) {
			logger.error("", e);
			throw new ConnectException(e.getMessage());
		}
	}

	private void connectOrUseConnection() throws ConnectException {
		if (session == null || !session.isConnected()) {
			doRealconnect();
		}
	}
	
	private void doRealconnect() throws ConnectException {
		try {
			/*
			 * Configure connexion
			 */
			JSch jsch = new JSch();
			jsch.addIdentity(configuration.getPublicKeyPath());
			session = jsch.getSession(configuration.getUser(), configuration.getHost(), configuration.getPort());
			UserInfo userInfo = new UserInfo() {

				public String getPassphrase() {
					return configuration.getPassPhrase();
				}

				public String getPassword() {
					return null;
				}

				public boolean promptPassphrase(String arg0) {
					logger.debug("Prompt Passphrase / {}", arg0);
					return true;
				}

				public boolean promptPassword(String arg0) {
					logger.debug("Prompt Password / {}", arg0);
					return true;
				}

				public boolean promptYesNo(String arg0) {
					logger.debug("Prompt YesNo / {}", arg0);
					return true;
				}

				public void showMessage(String arg0) {
					logger.debug("Message: {}", arg0);
				}

			};

			logger.info("Connecting to {} as {}...", configuration.getHost(), configuration.getUser());
			session.setUserInfo(userInfo);
			session.connect();
			logger.info("Successfully connected !");

			/*
			 * Connect
			 */
		} catch (JSchException e) {
			throw new ConnectException(e.getMessage());
		}

	}

	public void disconnect() {
		logger.info("Disconnecting from {} as {}...", configuration.getHost(), configuration.getUser());
		session.disconnect();
		logger.info("Successfully disconnected !");
	}

	// ----------------- Getters & Setters ------------------

	private void loadConfiguration(IConfigurationManager configurationManager, SshConfiguration defaultConfiguration) throws IOException {
		if (configurationManager != null && defaultConfiguration != null) {
			configuration = (SshConfiguration) configurationManager.loadConfiguration(defaultConfiguration);
		}
	}

	public SshConfiguration getDefaultConfiguration() {
		return defaultConfiguration;
	}

	public void setDefaultConfiguration(SshConfiguration defaultConfiguration) throws IOException {
		this.defaultConfiguration = defaultConfiguration;
		loadConfiguration(configurationManager, defaultConfiguration);
	}

	public IConfigurationManager getConfigurationManager() {
		return configurationManager;
	}

	public void setConfigurationManager(IConfigurationManager configurationManager) throws IOException {
		this.configurationManager = configurationManager;
		loadConfiguration(configurationManager, defaultConfiguration);
	}

}
